造型生成网站 - 设计稿

1. 目标与范围

1.1 项目目标

  • 核心目标:用户上传照片,系统生成"新造型图像 + 造型理由",形成可展示的前后对比与文字说明
  • 技术目标:构建可扩展的 AI 模型调用架构,支持灵活切换不同 Vision LLM 和生图模型

1.2 MVP 范围

包含 不包含(后期迭代)
单人头像/半身照输入 多人合照处理
1 张新造型图 + 1 段中文说明 多风格批量生成
基础上传与展示 复杂编辑工具
模型可配置切换 用户账号体系
临时图片存储 历史记录持久化

2. 用户流程(前台)

2.1 主流程

image-d1e6919c

2.2 页面结构

/                    # 首页(上传入口)
/result/:taskId      # 结果页(支持分享链接)

3. 系统架构(后台)

3.1 整体架构图

diagram-1769715951544-7a7cd582

3.2 核心组件说明

组件 职责 技术选型
Frontend 上传、进度展示、结果渲染 Next.js 14 + React 18 + TailwindCSS
Backend API 请求处理、任务编排、结果返回 Node.js + Express + TypeScript
Task Orchestrator 编排 AI 调用流程 自研状态机
Model Adapter 统一模型调用接口 适配器模式
Image Processor 图片压缩、格式转换、裁剪 sharp
Storage Service 临时文件存储 本地文件系统 / S3 兼容存储

4. 模型配置设计(核心)

4.1 配置文件结构

YAML

# config/models.yaml

# ============ Vision LLM 配置 ============
vision:
  # 当前激活的 provider
  active: "openai"

  providers:
    openai:
      name: "GPT-4o"
      endpoint: "https://api.openai.com/v1/chat/completions"
      model: "gpt-4o"
      apiKeyEnv: "OPENAI_API_KEY"
      maxTokens: 2000
      temperature: 0.7
      timeout: 60000

    anthropic:
      name: "Claude 3.5 Sonnet"
      endpoint: "https://api.anthropic.com/v1/messages"
      model: "claude-3-5-sonnet-20241022"
      apiKeyEnv: "ANTHROPIC_API_KEY"
      maxTokens: 2000
      timeout: 60000

    google:
      name: "Gemini 1.5 Pro"
      endpoint: "https://generativelanguage.googleapis.com/v1beta/models"
      model: "gemini-1.5-pro"
      apiKeyEnv: "GOOGLE_API_KEY"
      maxTokens: 2000
      timeout: 60000

    alibaba:
      name: "Qwen-VL-Max"
      endpoint: "https://dashscope.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation"
      model: "qwen-vl-max"
      apiKeyEnv: "DASHSCOPE_API_KEY"
      maxTokens: 2000
      timeout: 60000

# ============ 图像生成模型配置 ============
imageGen:
  # 当前激活的 provider
  active: "openai"

  providers:
    openai:
      name: "DALL-E 3"
      endpoint: "https://api.openai.com/v1/images/generations"
      model: "dall-e-3"
      apiKeyEnv: "OPENAI_API_KEY"
      size: "1024x1024"
      quality: "hd"
      timeout: 120000

    replicate_flux:
      name: "Flux 1.1 Pro"
      endpoint: "https://api.replicate.com/v1/predictions"
      model: "black-forest-labs/flux-1.1-pro"
      apiKeyEnv: "REPLICATE_API_TOKEN"
      aspectRatio: "1:1"
      outputFormat: "webp"
      timeout: 120000

    replicate_sdxl:
      name: "SDXL + InstantID"
      endpoint: "https://api.replicate.com/v1/predictions"
      model: "zsxkib/instant-id"
      apiKeyEnv: "REPLICATE_API_TOKEN"
      timeout: 180000
      # 需要额外传入 face_image
      requiresFaceImage: true

    fal_flux:
      name: "Fal Flux Pro"
      endpoint: "https://fal.run/fal-ai/flux-pro"
      apiKeyEnv: "FAL_KEY"
      imageSize: "square_hd"
      timeout: 120000

    midjourney:
      name: "Midjourney (via Proxy)"
      endpoint: "${MIDJOURNEY_PROXY_URL}"
      apiKeyEnv: "MIDJOURNEY_API_KEY"
      timeout: 300000
      # MJ 需要轮询获取结果
      pollingMode: true
      pollingInterval: 5000

# ============ 人脸检测配置(可选)============
faceDetection:
  enabled: true
  provider: "local"  # local / cloud

  providers:
    local:
      # 使用 face-api.js
      modelPath: "./models/face-api"
      minConfidence: 0.5

    cloud:
      # 使用云服务
      endpoint: "${FACE_API_ENDPOINT}"
      apiKeyEnv: "FACE_API_KEY"

# ============ 通用配置 ============
common:
  # 请求重试
  retry:
    maxAttempts: 3
    backoffMs: 1000
    backoffMultiplier: 2

  # 并发限制
  rateLimit:
    maxConcurrent: 10
    requestsPerMinute: 30

4.2 环境变量配置

# .env.example

# ===== Vision LLM API Keys =====
OPENAI_API_KEY=sk-xxx
ANTHROPIC_API_KEY=sk-ant-xxx
GOOGLE_API_KEY=AIza-xxx
DASHSCOPE_API_KEY=sk-xxx

# ===== Image Generation API Keys =====
REPLICATE_API_TOKEN=r8_xxx
FAL_KEY=xxx
MIDJOURNEY_PROXY_URL=https://your-mj-proxy.com
MIDJOURNEY_API_KEY=xxx

# ===== Optional Services =====
FACE_API_ENDPOINT=
FACE_API_KEY=

# ===== Storage =====
STORAGE_TYPE=local  # local / s3
STORAGE_PATH=./uploads
# S3_BUCKET=xxx
# S3_REGION=xxx
# S3_ACCESS_KEY=xxx
# S3_SECRET_KEY=xxx

# ===== Server =====
PORT=3001
NODE_ENV=development

4.3 模型适配器接口设计

// types/models.ts

// ===== Vision LLM 接口 =====
interface VisionAnalysisRequest {
  imageBase64: string;
  mimeType: string;
  prompt: string;
}

interface VisionAnalysisResponse {
  imagePrompt: string;      // 提取的 ###IMAGE_PROMPT###
  reasoning: string;        // 提取的 ###REASONING###
  rawResponse: string;      // 原始响应(调试用)
  usage?: {
    promptTokens: number;
    completionTokens: number;
  };

interface VisionProvider {
  name: string;
  analyze(request: VisionAnalysisRequest): Promise<VisionAnalysisResponse>;
  healthCheck(): Promise<boolean>;
}

// ===== Image Generation 接口 =====
interface ImageGenerationRequest {
  prompt: string;
  referenceImageBase64?: string;  // 用于人脸一致性模型
  size?: string;
  style?: string;
}

interface ImageGenerationResponse {
  imageUrl: string;         // 生成图片 URL
  imageBase64?: string;     // 可选返回 base64
  revisedPrompt?: string;   // 模型修正后的提示词
}

interface ImageGenProvider {
  name: string;
  generate(request: ImageGenerationRequest): Promise<ImageGenerationResponse>;
  healthCheck(): Promise<boolean>;
}

4.4 模型工厂模式

TypeScript

// services/modelFactory.ts

class ModelFactory {
  private visionProviders: Map<string, VisionProvider>;
  private imageGenProviders: Map<string, ImageGenProvider>;
  private config: ModelConfig;

  constructor(configPath: string) {
    this.config = this.loadConfig(configPath);
    this.visionProviders = new Map();
    this.imageGenProviders = new Map();
    this.initializeProviders();
  }

  // 获取当前激活的 Vision 提供者
  getVisionProvider(): VisionProvider {
    const active = this.config.vision.active;
    return this.visionProviders.get(active);
  }

  // 获取当前激活的生图提供者
  getImageGenProvider(): ImageGenProvider {
    const active = this.config.imageGen.active;
    return this.imageGenProviders.get(active);
  }

  // 动态切换提供者(运行时)
  switchVisionProvider(providerName: string): void;
  switchImageGenProvider(providerName: string): void;

  // 获取所有可用提供者(用于管理界面)
  listProviders(): { vision: string[], imageGen: string[] };
}

5. 核心流程设计

5.1 主流程时序图

diagram-1769716264901-33c82a37

5.2 任务状态机

diagram-1769716390560-900198c8

6. 元提示词设计

6.1 系统提示词模板

# config/prompts.yaml

systemPrompt: |
  你是一位世界顶级的发型设计师与形象顾问,拥有20年服务名人与普通客户的经验。
  你擅长根据客户的脸型、五官特征、气质类型,设计最适合的发型与整体造型方案。

  ## 你的任务
  分析用户上传的照片,为其设计一个全新的造型方案,并提供专业的设计理由。

  ## 分析维度
  1. **脸型分析**:椭圆/圆形/方形/长形/心形/菱形
  2. **五官特征**:眼睛大小、鼻型、嘴唇、额头高度、下颌线条
  3. **当前状态**:现有发型、发质推测、整体风格
  4. **气质类型**:知性/甜美/帅气/成熟/清新/...

  ## 输出要求
  请严格按照以下格式输出,使用分隔符分隔:

  ###IMAGE_PROMPT###
  (这里输出英文的图像生成提示词,要求:
   - 必须强调 "same person, preserve exact facial features, same face"
   - 详细描述新发型:长度、层次、刘海、颜色、质感
   - 描述服装风格(如适用)
   - 指定摄影风格:lighting, camera angle, background
   - 指定图像质量:professional photography, 8k, detailed
   - 示例结构:A portrait of the same person with [新发型描述], wearing [服装], [摄影风格], [质量词])

  ###REASONING###
  (这里输出中文的造型设计说明,包含:
   - 脸型与五官分析结果
   - 为什么推荐这个发型(解决什么问题/强化什么优点)
   - 新造型会带来的气质变化
   - 日常打理建议(可选)
   - 总字数控制在 150-250 字)

userPrompt: |
  请分析这张照片中的人物,为 TA 设计一个全新的造型方案。

6.2 输出解析器

// utils/promptParser.ts

interface ParsedResponse {
  imagePrompt: string;
  reasoning: string;
  parseSuccess: boolean;
  errors?: string[];
}

function parseVisionResponse(rawResponse: string): ParsedResponse {
  const imagePromptMatch = rawResponse.match(
    /###IMAGE_PROMPT###\s*([\s\S]*?)(?=###REASONING###|$)/
  );
  const reasoningMatch = rawResponse.match(
    /###REASONING###\s*([\s\S]*?)$/
  );

  const result: ParsedResponse = {
    imagePrompt: imagePromptMatch?.[1]?.trim() || '',
    reasoning: reasoningMatch?.[1]?.trim() || '',
    parseSuccess: true,
    errors: []
  };

  // 验证
  if (!result.imagePrompt) {
    result.parseSuccess = false;
    result.errors.push('Missing IMAGE_PROMPT section');
  }
  if (!result.reasoning) {
    result.parseSuccess = false;
    result.errors.push('Missing REASONING section');
  }

  return result;
}

7. API 接口设计

7.1 接口清单

# API Endpoints

POST /api/upload:
  description: 上传图片并创建任务
  request:
    type: multipart/form-data
    fields:
      image: File (required, max 10MB, jpg/png/webp)
  response:
    200:
      taskId: string
      status: "pending"
      message: "任务已创建"
    400:
      error: "INVALID_IMAGE" | "NO_FACE_DETECTED" | "MULTIPLE_FACES"
      message: string
    429:
      error: "RATE_LIMITED"
      message: string

GET /api/status/:taskId:
  description: 查询任务状态
  response:
    200:
      taskId: string
      status: "pending" | "analyzing" | "generating" | "completed" | "failed"
      progress: number (0-100)
      message: string
      resultUrl?: string  # completed 时返回
      error?: string      # failed 时返回
    404:
      error: "TASK_NOT_FOUND"

GET /api/result/:taskId:
  description: 获取任务结果
  response:
    200:
      taskId: string
      originalImageUrl: string
      generatedImageUrl: string
      reasoning: string
      createdAt: string
      expiresAt: string
    404:
      error: "TASK_NOT_FOUND" | "RESULT_EXPIRED"

GET /api/image/:imageId:
  description: 获取图片(代理/签名URL)
  response:
    200: Binary (image/*)
    404: Not Found

POST /api/admin/switch-model:
  description: 切换模型(管理接口)
  headers:
    Authorization: Bearer <admin_token>
  request:
    type: "vision" | "imageGen"
    provider: string
  response:
    200:
      success: true
      activeProvider: string

7.2 错误码规范

// constants/errorCodes.ts

export const ErrorCodes = {
  // 图片相关 (1xxx)
  INVALID_IMAGE_FORMAT: { code: 1001, message: '不支持的图片格式,请上传 JPG/PNG/WebP' },
  IMAGE_TOO_LARGE: { code: 1002, message: '图片过大,请上传 10MB 以内的图片' },
  IMAGE_TOO_SMALL: { code: 1003, message: '图片分辨率过低,请上传更清晰的照片' },
  NO_FACE_DETECTED: { code: 1004, message: '未检测到人脸,请上传包含清晰正面人脸的照片' },
  MULTIPLE_FACES: { code: 1005, message: '检测到多张人脸,请上传单人照片' },
  FACE_TOO_SMALL: { code: 1006, message: '人脸区域过小,请上传脸部更清晰的照片' },

  // 任务相关 (2xxx)
  TASK_NOT_FOUND: { code: 2001, message: '任务不存在' },
  TASK_EXPIRED: { code: 2002, message: '任务已过期' },
  TASK_IN_PROGRESS: { code: 2003, message: '任务处理中,请稍候' },

  // AI 模型相关 (3xxx)
  VISION_ANALYSIS_FAILED: { code: 3001, message: 'AI 分析失败,请重试' },
  IMAGE_GENERATION_FAILED: { code: 3002, message: '图像生成失败,请重试' },
  MODEL_UNAVAILABLE: { code: 3003, message: 'AI 服务暂时不可用' },
  CONTENT_POLICY_VIOLATION: { code: 3004, message: '图片内容不符合使用规范' },

  // 系统相关 (4xxx)
  RATE_LIMITED: { code: 4001, message: '请求过于频繁,请稍后再试' },
  SERVER_ERROR: { code: 4002, message: '服务器错误,请稍后重试' },
  SERVICE_UNAVAILABLE: { code: 4003, message: '服务维护中' },
} as const;

8. 数据模型

8.1 任务模型

// types/task.ts

interface Task {
  id: string;                    // UUID
  status: TaskStatus;
  progress: number;              // 0-100

  // 输入
  originalImage: {
    path: string;
    url: string;
    mimeType: string;
    size: number;
  };

  // Vision 分析结果
  analysis?: {
    imagePrompt: string;
    reasoning: string;
    rawResponse: string;
    completedAt: Date;
  };

  // 生成结果
  generation?: {
    imageUrl: string;
    localPath: string;
    revisedPrompt?: string;
    completedAt: Date;
  };

  // 错误信息
  error?: {
    code: number;
    message: string;
    details?: string;
    stage: 'upload' | 'analysis' | 'generation';
  };

  // 元数据
  createdAt: Date;
  updatedAt: Date;
  expiresAt: Date;               // 默认 24h 后过期

  // 配置快照(记录使用的模型)
  modelConfig: {
    visionProvider: string;
    imageGenProvider: string;
  };

type TaskStatus =
  | 'pending'
  | 'analyzing'
  | 'generating'
  | 'completed'
  | 'failed';

8.2 存储策略

本地存储结构:
uploads/
├── tasks/
│   ├── {taskId}/
│   │   ├── original.jpg        # 原图
│   │   ├── generated.jpg       # 生成图
│   │   └── metadata.json       # 任务元数据
│   └── ...
└── temp/                        # 临时文件(处理中)

清理策略:
- 已完成任务: 24 小时后清理
- 失败任务: 6 小时后清理
- 临时文件: 1 小时后清理

9. 前端设计

9.1 页面组件结构

src/
├── app/
│   ├── page.tsx                 # 首页
│   ├── result/[taskId]/
│   │   └── page.tsx             # 结果页
│   └── layout.tsx
├── components/
│   ├── upload/
│   │   ├── DropZone.tsx         # 拖拽上传区
│   │   ├── ImagePreview.tsx     # 上传预览
│   │   └── UploadButton.tsx
│   ├── progress/
│   │   ├── ProgressBar.tsx      # 进度条
│   │   ├── StatusMessage.tsx    # 状态文字
│   │   └── LoadingAnimation.tsx
│   ├── result/
│   │   ├── BeforeAfter.tsx      # 前后对比
│   │   ├── ReasoningCard.tsx    # 造型说明
│   │   └── ActionButtons.tsx    # 下载/分享/重试
│   └── common/
│       ├── Header.tsx
│       ├── Footer.tsx
│       └── ErrorBoundary.tsx
├── hooks/
│   ├── useUpload.ts
│   ├── useTaskStatus.ts         # 轮询任务状态
│   └── useImagePreload.ts
├── lib/
│   ├── api.ts                   # API 调用封装
│   └── utils.ts
└── styles/
    └── globals.css

9.2 状态管理流程

// hooks/useTaskStatus.ts

interface TaskState {
  taskId: string | null;
  status: TaskStatus;
  progress: number;
  message: string;
  result: TaskResult | null;
  error: TaskError | null;
}

function useTaskStatus(taskId: string | null) {
  const [state, setState] = useState<TaskState>(initialState);

  useEffect(() => {
    if (!taskId) return;

    const pollInterval = setInterval(async () => {
      const response = await api.getTaskStatus(taskId);

      setState(prev => ({
        ...prev,
        status: response.status,
        progress: response.progress,
        message: getStatusMessage(response.status),
      }));

      if (response.status === 'completed') {
        clearInterval(pollInterval);
        const result = await api.getTaskResult(taskId);
        setState(prev => ({ ...prev, result }));
      }

      if (response.status === 'failed') {
        clearInterval(pollInterval);
        setState(prev => ({ ...prev, error: response.error }));
      }
    }, 2000);

    return () => clearInterval(pollInterval);
  }, [taskId]);

  return state;
}

10. 安全与性能

10.1 安全措施

风险 措施
恶意文件上传 文件类型白名单 + magic bytes 校验
图片内容违规 可接入内容审核 API(腾讯云/阿里云)
API 滥用 基于 IP 的速率限制 + 可选验证码
敏感数据泄露 任务 ID 使用 UUID、图片 URL 签名
隐私保护 默认 24h 自动清理、不做持久化存储

10.2 性能优化

场景 策略
图片上传 前端压缩至 2048px max、使用 WebP
长任务等待 轮询间隔动态调整(2s → 5s)
结果页加载 图片渐进式加载 + 骨架屏
API 响应 压缩响应、CDN 加速静态资源

11. 部署架构

11.1 开发环境

本地开发:
- Node.js 18+
- pnpm
- 本地文件存储

11.2 生产环境

┌─────────────────────────────────────────────────────────┐
│                        CDN                               │
│                    (静态资源)                            │
└──────────────────────────┬──────────────────────────────┘
                           │
┌──────────────────────────▼──────────────────────────────┐
│                   Load Balancer                          │
│                   (Nginx/云LB)                           │
└──────────────────────────┬──────────────────────────────┘
                           │
        ┌──────────────────┼──────────────────┐
        ▼                  ▼                  ▼
┌──────────────┐  ┌──────────────┐  ┌──────────────┐
│   App Node   │  │   App Node   │  │   App Node   │
│   (PM2)      │  │   (PM2)      │  │   (PM2)      │
└──────┬───────┘  └──────┬───────┘  └──────┬───────┘
       │                 │                 │
       └────────────────┬┼─────────────────┘
                        ││
        ┌───────────────┘└───────────────┐
        ▼                                ▼
┌──────────────┐                ┌──────────────┐
│    Redis     │                │  S3/MinIO    │
│  (任务状态)   │                │  (文件存储)   │
└──────────────┘                └──────────────┘

VibeCoding 导航:⬅️ 02-页面清单 | 01-造型生成网站 - 设计稿 | ➡️ 02-计划书